This project is part of the
@thi.ng/umbrella monorepo and anti-framework.
About
Lightweight, reactive, VDOM-less UI/DOM components with async lifecycle and @thi.ng/hiccup compatible.
From hdom to rdom: Reactive UIs without virtual DOMs
In many ways this package is the direct successor of
@thi.ng/hdom,
which for several years was my preferred way of building UIs. hdom eschewed
using a virtual DOM to represent and maintain a dynamic tree of (UI) components
and instead only required a previous and current component tree in
@thi.ng/hiccup
format (aka nested, plain JS arrays w/ optional support for embedded other JS
data types, like ES6 iterables, @thi.ng/api
interfaces, etc.)
to perform its UI updates. Yet, whilst hiccup trees are plain, simple, user
defined data structures, which can be very easily composed without any
libraries, hdom itself was still heavily influenced by the general vDOM
approach and therefore a centralized update cycle and computing differences
between the trees were necessary evils core tasks. In short, hdom allowed
the illusion of declarative components with reactive state updates, but had to
use a complex and recursive diff to realize those updates.
In contrast, _@thi.ng/rdom_ directly supports embedding reactive
values/components in the hiccup tree and compiles them in such a way that their
value changes directly target underlying DOM nodes without having to resort to
any other intermediate processing (no diffing, vDOM updates etc.).
_@thi.ng/rdom_ is entirely vDOM-free. It supports declarative component
definitions via
@thi.ng/hiccup,
@thi.ng/rstream,
ES6 classes, direct DOM manipulation (incl. provided helpers) and/or any mixture
of these approaches.
Targetted, isolated updates
If a reactive value is used for an element attribute, a value change will
trigger an update of only that attribute (there's special handling for event
listeners, CSS classes, data attributes and style
attribs). If a reactive
value is used as (text) body of an element (or an element/component itself),
only that body/subtree in the target DOM will be impacted/updated directly...
The package provides an interface
IComponent
(with a super simple life cycle API), a base component class
Component
for
stubbing and a number of fundamental control constructs & component-wrappers for
composing more complex components and to reduce boilerplate for various
situations. Whilst targetting a standard JS DOM by default, each component can
decide for itself what kind of target data structure (apart from a browser DOM)
it manages. rdom components themselves have no mandatory knowledge of a
browser DOM. As an example, similar to
@thi.ng/hdom-canvas,
the
@thi.ng/rdom-canvas
wrapper provides a component which subscribes to a stream of hiccup-based scene
descriptions (trees) and then translates each scene-value into HTML Canvas API
draw calls.
Async updates, scheduling & life cycle methods
Since there's no central coordination in rdom (neither explicitly nor
implicitly), each component can (and does) update whenever its state value has
changed. Likewise, components are free to directly manipulate the DOM through
other means, as hinted at earlier. Various rdom control constructs are
dispatching component updates via a central scheduler. By default this is only a
dummy implementation which processes tasks immediately. However, as usual rdom
only relies on the
IScheduler
interface and so supports other implementations, like
RAFScheduler
.
The IComponent
interface is at the heart of rdom. It defines three lifecycle methods to:
.mount()
, .unmount()
and .update()
a component. The first two are always
async
to allow for more complex component initialization procedures (e.g.
preloaders, WASM init, other async ops...). Several of the higher-order
controller components/constructs too demand async
functions for the same
reasons.
Because rdom itself relies for most reactive features, stream composition and
reactive value transformations on other packages, i.e.
@thi.ng/rstream
and
@thi.ng/transducers,
please consult the docs for these packages to learn more about the available
constructs and patterns. Most of rdom only deals with either subscribing to
reactive values and/or wrapping/transforming existing subscriptions, either
explicitly using the provided control components (e.g.
$sub()
) or using
$compile()
to
auto-wrap such values embedded in an hiccup tree.
@thi.ng/atom integration
For the sake of deduplication of functionality and to keep the number of
dependencies to a minimum, direct
@thi.ng/atom
integration has been removed in favor of using relevant
@thi.ng/rstream
constructs, which can be used as lightweight adapters, i.e.:
DOM creation & mutation
The package provides many functions to simplify the creation of individual or
entire trees of DOM elements and to manipulate them at a later time. The single
most important function of the package is
$compile. It acts
as a facade for many of these other functions and creates an actual DOM from a
given hiccup component tree. It also automatically wraps any reactive values
contained therein.
All of these functions are also usable, even if you don't intend to use any
other package features!
Control structures
For more advanced usage, rdom provides a range of control structures (container
components) to simplify the handling of reactive states and reduce boilerplate
for the implementation of common UI structures (e.g. item lists of any kind).
The following links lead to the documentation of these wrappers, incl. small
code examples:
Event handlers for reactive streams
Reactive rdom component are based on
@thi.ng/rstream
subscriptions. In order to create a feedback loop between those reactive state
values and their subscribed UI components, input event handlers need to feed any
user changes back to those reactive state(s). To reduce boilerplate for these
tasks, the following higher order input event handlers are provided:
import { $compile, $input } from "@thi.ng/rdom";
import { reactive, trace } from "@thi.ng/rstream";
const name = reactive("").map((x) => x.toUpperCase());
$compile(["input", {
type: "text",
oninput: $input(name),
value: name
}]).mount(document.body);
name.subscribe(trace("name:"));
Click counter:
import { $compile, $inputTrigger } from "@thi.ng/rdom";
import { reactive } from "@thi.ng/rstream";
import { count, scan } from "@thi.ng/transducers";
const clicks = reactive(true);
$compile([
"button",
{ onclick: $inputTrigger(clicks) },
"clicks: ",
clicks.transform(scan(count(-1))),
]).mount(document.body);
Status
STABLE - used in production
Search or submit any issues for this package
Support packages
Related packages
Installation
yarn add @thi.ng/rdom
ES module import:
<script type="module" src="https://cdn.skypack.dev/@thi.ng/rdom"></script>
Skypack documentation
For Node.js REPL:
const rdom = await import("@thi.ng/rdom");
Package sizes (brotli'd, pre-treeshake): ESM: 4.06 KB
Dependencies
Usage examples
Several projects in this repo's
/examples
directory are using this package:
Screenshot | Description | Live demo | Source |
---|
| Large ASCII font text generator using @thi.ng/rdom | Demo | Source |
| Self-modifying, animated typographic grid with emergent complex patterns | Demo | Source |
| Probabilistic color theme generator | Demo | Source |
| Color palette generation via dominant color extraction from uploaded images | Demo | Source |
| Interactive visualization of closest points on ellipses | Demo | Source |
| Fiber-based cooperative multitasking basics | Demo | Source |
| Randomized space-filling, nested grid layout generator | Demo | Source |
| Browser REPL for a Lispy S-expression based mini language | Demo | Source |
| Mastodon API feed reader with support for different media types, fullscreen media modal, HTML rewriting | Demo | Source |
| Basic thi.ng/meta-css usage & testbed | Demo | Source |
| Parser grammar livecoding editor/playground & codegen | Demo | Source |
| Matrix-based image color adjustments | Demo | Source |
| Randomized 4-point 2D color gradient image generator | Demo | Source |
| Interactive pixel sorting tool using thi.ng/color & thi.ng/pixel | Demo | Source |
| RGB waveform image analysis | Demo | Source |
| Live coding playground for 2D geometry generation using @thi.ng/pointfree-lang | Demo | Source |
| Animated, iterative polygon subdivisions & visualization | Demo | Source |
| Procedural stochastic text generation via custom DSL, parse grammar & AST transformation | Demo | Source |
| Demonstates various rdom usage patterns | Demo | Source |
| Dynamically loaded images w/ preloader state | Demo | Source |
| rdom drag & drop example | Demo | Source |
| Basic usage of the declarative rdom-forms generator | Demo | Source |
| rstream & transducer-based FSM for converting key event sequences into high-level commands | Demo | Source |
| rdom & hiccup-canvas interop test | Demo | Source |
| Full umbrella repo doc string search w/ paginated results | Demo | Source |
| rdom powered SVG graph with draggable nodes | Demo | Source |
| Responsive image gallery with tag-based Jaccard similarity ranking | Demo | Source |
| Generative audio synth offline renderer and WAV file export | Demo | Source |
| Minimal rstream sync() example using rdom | Demo | Source |
| Responsive & reactively computed stacked column layout | Demo | Source |
| SVG path parsing & dynamic resampling | Demo | Source |
| Multi-layer vectorization & dithering of bitmap images | Demo | Source |
| rdom & WebGL-based image channel editor | Demo | Source |
API
Generated API docs
TODO
Currently, documentation only exists in the form of small examples and various
doc strings (incomplete). I'm working to alleviate this situation ASAP... In
that respect, PRs are welcome as well!
Basic usage
import { $compile } from "@thi.ng/rdom";
import { reactive } from "@thi.ng/rstream";
import { cycle, map } from "@thi.ng/transducers";
const bg = reactive("gray");
const colors = cycle(["magenta", "yellow", "cyan"]);
const nextColor = () => bg.next(<string>colors.next().value);
$compile([
"div",
{},
["h1", {}, bg.map((col) => `Hello, ${col}!`)],
[
"button#foo.w4.pa3.bn",
{
style: { background: bg },
onclick: nextColor,
},
bg,
],
]).mount(document.body);
Lists
See $list
and
$klist
docs for
further information...
import { $klist } from "@thi.ng/rdom";
import { reactive } from "@thi.ng/rstream";
const items = reactive([
{ id: "a", val: 1 },
{ id: "b", val: 2 },
{ id: "c", val: 3 },
]);
$klist(
items,
"ul",
{ class: "list red" },
(x) => ["li", {}, x.id, ` (${x.val})`],
(x) => `${x.id}-${x.val}`
).mount(document.body);
setTimeout(
() => {
items.next([
{ id: "b", val: 2 },
{ id: "d", val: 4 },
{ id: "c", val: 30 },
]);
},
1000
);
Authors
If this project contributes to an academic publication, please cite it as:
@misc{thing-rdom,
title = "@thi.ng/rdom",
author = "Karsten Schmidt",
note = "https://thi.ng/rdom",
year = 2020
}
License
© 2020 - 2023 Karsten Schmidt // Apache License 2.0